#include "ftp_uploader.h"

#include "curl.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <sstream>
#include <iomanip>

namespace fs = std::filesystem;

FtpUploader::FtpUploader(const std::string& user,
                         const std::string& pass,
                         const std::string& remoteDir,
                         int threadCount)
    : m_user(user),
      m_pass(pass),
      m_remoteBase(remoteDir),
      m_threadCount(threadCount)
{
    // normalize trailing slash: keep without trailing '/' to make appends consistent
    if (!m_remoteBase.empty() && m_remoteBase.back() == '/') m_remoteBase.pop_back();
    curl_global_init(CURL_GLOBAL_DEFAULT);
}

FtpUploader::FtpUploader(const std::string& user,
                         const std::string& pass,
                         const std::string& remoteDir,
                         const std::vector<std::pair<std::string,std::string>>& files,
                         const std::string& reportFile,
                         int threadCount)
    : m_user(user),
      m_pass(pass),
      m_remoteBase(remoteDir),
      m_threadCount(threadCount),
      m_reportFile(reportFile),
      m_filePairs(files)
{
    if (!m_remoteBase.empty() && m_remoteBase.back() == '/') m_remoteBase.pop_back();
    curl_global_init(CURL_GLOBAL_DEFAULT);
}

FtpUploader::~FtpUploader() {
    curl_global_cleanup();
}

void FtpUploader::setFiles(const std::vector<std::string>& files) {
    std::lock_guard<std::mutex> lg(m_idxMutex);
    m_files = files;
    m_filePairs.clear();
    // convert local-only list to filePairs with relative name = filename
    for (const auto& p : files) {
        fs::path path(p);
        m_filePairs.emplace_back(p, path.filename().generic_string());
    }
    m_nextIndex = 0;
}

void FtpUploader::setFilePairs(const std::vector<std::pair<std::string,std::string>>& filePairs) {
    std::lock_guard<std::mutex> lg(m_idxMutex);
    m_filePairs = filePairs;
    m_nextIndex = 0;
}

static std::string ensure_trailing_slash(const std::string& s) {
    if (s.empty()) return std::string();
    if (s.back() == '/') return s;
    return s + "/";
}

bool FtpUploader::createRemoteDirAt(const std::string& remoteDir) {
    if (remoteDir.empty()) return false;

    // normalize folderUrl: ensure trailing slash
    std::string folderUrl = remoteDir;
    if (folderUrl.back() != '/') folderUrl += '/';

    // split scheme+host and path
    std::string baseUrl;
    std::string pathPart;
    {
        size_t pos = folderUrl.find("://");
        if (pos != std::string::npos) {
            size_t slash = folderUrl.find('/', pos + 3);
            if (slash != std::string::npos) {
                baseUrl = folderUrl.substr(0, slash + 1); // include trailing '/'
                pathPart = folderUrl.substr(slash + 1);   // rest (may include trailing '/')
            } else {
                baseUrl = folderUrl;
                pathPart.clear();
            }
        } else {
            // no scheme -> treat whole as path
            baseUrl = "/";
            pathPart = folderUrl;
        }
    }
    if (!pathPart.empty() && pathPart.back() == '/') pathPart.pop_back();

    CURL* curl = curl_easy_init();
    if (!curl) return false;

    if (!m_user.empty() || !m_pass.empty()) {
        std::string up = m_user + ":" + m_pass;
        curl_easy_setopt(curl, CURLOPT_USERPWD, up.c_str());
    }

    // Optional: enable verbose for debugging
    // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

    // Try per-level MKD relative to baseUrl
    bool anyCreated = false;
    bool mkdTried = false;
    if (!pathPart.empty()) {
        std::string cur;
        size_t start = 0;
        while (start < pathPart.size()) {
            auto next = pathPart.find('/', start);
            if (next == std::string::npos) next = pathPart.size();
            std::string part = pathPart.substr(start, next - start);
            if (!part.empty()) {
                if (!cur.empty()) cur += "/";
                cur += part; // cur is "a", then "a/b", ...
                std::string mkdCmd = "MKD " + cur;
                struct curl_slist* quote = nullptr;
                quote = curl_slist_append(quote, mkdCmd.c_str());

                curl_easy_setopt(curl, CURLOPT_URL, baseUrl.c_str());
                curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
                curl_easy_setopt(curl, CURLOPT_QUOTE, quote);

                CURLcode r = curl_easy_perform(curl);
                curl_slist_free_all(quote);
                mkdTried = true;
                if (r == CURLE_OK) anyCreated = true;
                // ignore non-OK: could already exist or server responds differently
            }
            start = next + 1;
        }
    }

    // If MKD attempts worked (some created) -> done
    if (mkdTried && anyCreated) {
        curl_easy_cleanup(curl);
        return true;
    }

    // Fallback: zero-byte dummy upload with CURLOPT_FTP_CREATE_MISSING_DIRS
    const std::string dummyName = ".curl_mkdir_dummy.tmp";
    std::string dummyUrl = folderUrl + dummyName;

    curl_easy_setopt(curl, CURLOPT_URL, dummyUrl.c_str());
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)0);
    curl_easy_setopt(curl, CURLOPT_READFUNCTION, [](char*, size_t, size_t, void*) -> size_t { return 0; });
    curl_easy_setopt(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR);
    curl_easy_setopt(curl, CURLOPT_FTP_USE_EPSV, 1L);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        std::cerr << "[createRemoteDirAt] dummy upload failed: " << curl_easy_strerror(res) << "\n";
        // continue — best-effort
    } else {
        // attempt to delete dummy in directory context
        struct curl_slist* q = nullptr;
        std::string deleCmd = "DELE " + dummyName;
        q = curl_slist_append(q, deleCmd.c_str());
        // set URL to folder context for DELE
        curl_easy_setopt(curl, CURLOPT_URL, folderUrl.c_str());
        curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
        curl_easy_setopt(curl, CURLOPT_QUOTE, q);
        CURLcode r2 = curl_easy_perform(curl);
        if (r2 != CURLE_OK) {
            std::cerr << "[createRemoteDirAt] dummy delete warning: " << curl_easy_strerror(r2) << "\n";
        }
        curl_slist_free_all(q);
        curl_easy_cleanup(curl);
        return true;
    }

    // If none succeeded, best-effort: return whether any MKD created something
    curl_easy_cleanup(curl);
    return anyCreated;
}

void FtpUploader::workerThreadFunc() {
    while (true) {
        std::pair<std::string,std::string> pair;
        {
            std::lock_guard<std::mutex> lg(m_idxMutex);
            if (m_nextIndex >= m_filePairs.size()) return;
            pair = m_filePairs[m_nextIndex++];
        }

        const std::string localPath = pair.first;
        bool ok = uploadSingleFilePair(pair);
        if (!ok) {
            std::cerr << "[Failed] upload " << pair.first<<":"<<pair.second << "\n";
        } else {
            std::cout << "[Successed] upload " << pair.first<< "\n";
        }
    }
}

void FtpUploader::run() 
{
    if (m_filePairs.empty()) {
        std::cerr << "[FtpUploader] No files to upload.\n";
        return;
    }

    // write csv header
    {
        std::lock_guard<std::mutex> lg(m_csvMutex);
        std::ofstream ofs(m_reportFile);
        ofs << "filename,local_bytes,uploaded_bytes,elapsed_ms,speed_MBps\n";
    }

    // --- PRE-CREATE remote dirs (single-threaded) ---
    std::unordered_set<std::string> uniqueDirs; // normalized without trailing slash

    for (const auto &p : m_filePairs) {
        std::string rel = p.second;
        for (auto &c : rel) if (c == '\\') c = '/';
        // compute parent dir of rel (if no '/', parent is remote base)
        size_t pos = rel.find_last_of('/');
        std::string remoteDir = m_remoteBase;
        if (!remoteDir.empty() && remoteDir.back() == '/') remoteDir.pop_back();

        if (pos != std::string::npos) {
            std::string relParent = rel.substr(0, pos); // e.g. a/b
            // normalize: ensure single '/'
            std::string cand = remoteDir;
            if (!cand.empty()) cand += '/';
            cand += relParent;
            // remove trailing slash if any
            if (!cand.empty() && cand.back() == '/') cand.pop_back();
            uniqueDirs.insert(cand);
        } else {
            // file at remote base
            if (!remoteDir.empty()) uniqueDirs.insert(remoteDir);
        }
    }

    // single-threaded create
    {
        std::lock_guard<std::mutex> lg(m_dirMutex);
        m_createdDirs.clear();
        m_failedDirs.clear();
    }

    for (const auto &d : uniqueDirs) {
        bool ok = createRemoteDirAt(d);
        {
            std::lock_guard<std::mutex> lg(m_dirMutex);
            if (ok) m_createdDirs.insert(d);
            else m_failedDirs.insert(d);
        }
        if (!ok) {
            std::cerr << "[run] Warning: pre-create remote dir failed: " << d << "\n";
        }
    }
    // --- pre-create done ---

    total_local_bytes.store(0);
    total_uploaded_bytes.store(0);
    m_nextIndex = 0;
    m_startTime = std::chrono::steady_clock::now();

    // start worker threads
    std::vector<std::thread> workers;
    workers.reserve(m_threadCount);
    std::cout << "[FtpUploader] Starting " << m_threadCount << " worker threads.\n";
    for (int i = 0; i < m_threadCount; i++) {
        workers.emplace_back(&FtpUploader::workerThreadFunc, this);
        std::cout << "[FtpUploader] Starting thread Uploader File: " << m_filePairs[i].first << "\n";
    }
    for (auto &t : workers) if (t.joinable()) t.join();

    m_endTime = std::chrono::steady_clock::now();

    uint64_t local = total_local_bytes.load();
    uint64_t uploaded = total_uploaded_bytes.load();
    double seconds = std::chrono::duration<double>(m_endTime - m_startTime).count(); // convert to minutes
    double overallMBps = (uploaded / 1024.0 / 1024.0) / (seconds > 0.0 ? seconds : 1.0);

    {
        std::lock_guard<std::mutex> lg(m_csvMutex);
        std::ofstream ofs(m_reportFile, std::ios::app);
        long long elapsedMs = static_cast<long long>(seconds * 1000.0);
        ofs << "FtpUploaderResult," << local << "," << uploaded << "," << elapsedMs << "," << overallMBps << "\n";
    }

    std::cout << "[FtpUploader] Done. local_bytes=" << local
              << " uploaded_bytes=" << uploaded
              << " duration(Min)=" << seconds/60.0
              << " overall_MBps=" << overallMBps << "\n";
}

bool FtpUploader::uploadSingleFilePair(const std::pair<std::string,std::string>& filePair) {
    const std::string localPath = filePair.first;
    std::string rel = filePair.second; // relative path, e.g. "subdir/file.bin"

    // local size
    uint64_t localSize = 0;
    try {
        localSize = static_cast<uint64_t>(fs::file_size(localPath));
    } catch (const std::exception& e) {
        std::cerr << "[upload] file_size failed for " << localPath << " : " << e.what() << "\n";
        return false;
    }
    if (localSize == 0) {
        std::cerr << "[upload] skip empty file: " << localPath << "\n";
        return false;
    }

    // normalize separators
    for (auto &c : rel) if (c == '\\') c = '/';

    // compute remoteDirForFile and filename
    std::string remoteDirForFile = m_remoteBase;
    if (!remoteDirForFile.empty() && remoteDirForFile.back() == '/') remoteDirForFile.pop_back();
    std::string filename;
    size_t pos = rel.find_last_of('/');
    if (pos == std::string::npos) {
        filename = rel;
        // remoteDirForFile remains m_remoteBase (no extra path)
    } else {
        filename = rel.substr(pos + 1);
        std::string relParent = rel.substr(0, pos);
        if (!relParent.empty()) {
            remoteDirForFile += '/';
            remoteDirForFile += relParent;
        }
    }

    // normalize cache key (no trailing slash)
    std::string cacheKey = remoteDirForFile;
    if (!cacheKey.empty() && cacheKey.back() == '/') cacheKey.pop_back();

    // check cache: if dir previously failed, we warn once and continue (best-effort)
    {
        std::lock_guard<std::mutex> lg(m_dirMutex);
        if (m_failedDirs.find(cacheKey) != m_failedDirs.end()) {
            std::cerr << "[upload] Warning: remote directory previously failed to create: " << cacheKey << " (continuing)\n";
        } else if (m_createdDirs.find(cacheKey) == m_createdDirs.end()) {
            // This should be rare (not in pre-create set) — try create once and cache
            bool ok = createRemoteDirAt(cacheKey);
            if (ok) m_createdDirs.insert(cacheKey);
            else m_failedDirs.insert(cacheKey);
        }
    }

    // open file
    FILE* fp = fopen(localPath.c_str(), "rb");
    if (!fp) {
        std::cerr << "[upload] open failed: " << localPath << "\n";
        return false;
    }

    std::string targetUrl = remoteDirForFile;
    if (!targetUrl.empty() && targetUrl.back() != '/') targetUrl += '/';
    targetUrl += filename;

    CURL* curl = curl_easy_init();
    if (!curl) { fclose(fp); return false; }

    if (!m_user.empty() || !m_pass.empty()) {
        std::string up = m_user + ":" + m_pass;
        curl_easy_setopt(curl, CURLOPT_USERPWD, up.c_str());
    }

    curl_easy_setopt(curl, CURLOPT_URL, targetUrl.c_str());
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_READDATA, fp);
    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(localSize));
    // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); // 开发时打开查看交互

    // perform with simple retry and verification
    int maxRetries = 2;
    int attempt = 0;
    curl_off_t uploaded = 0;
    CURLcode res = CURLE_OK;
    auto t0 = std::chrono::steady_clock::now();
    while (attempt <= maxRetries) {
        // rewind file before each attempt
        if (fseek(fp, 0, SEEK_SET) != 0) {
            std::cerr << "[upload] rewind failed for " << localPath << "\n";
            break;
        }

        auto s = std::chrono::steady_clock::now();
        res = curl_easy_perform(curl);
        auto e = std::chrono::steady_clock::now();

        uploaded = 0;
        curl_easy_getinfo(curl, CURLINFO_SIZE_UPLOAD_T, &uploaded);

        if (res == CURLE_OK && static_cast<uint64_t>(uploaded) == localSize) {
            // success
            t0 = s;
            t0 = std::chrono::time_point_cast<std::chrono::steady_clock::duration>(s);
            t0 = s;
            t0 = s; // keep times (we'll compute duration below)
            break;
        }

        // not fully uploaded: log & retry
        ++attempt;
        std::cerr << "[upload] attempt " << attempt << " for " << filename
                  << " uploaded=" << uploaded << " local=" << localSize
                  << " err=" << curl_easy_strerror(res) << " -> retrying\n";

        if (attempt > maxRetries) break;
    }
    auto t1 = std::chrono::steady_clock::now();

    // update totals
    total_local_bytes.fetch_add(localSize, std::memory_order_relaxed);
    total_uploaded_bytes.fetch_add(static_cast<uint64_t>(uploaded), std::memory_order_relaxed);

    curl_easy_cleanup(curl);
    fclose(fp);

    long long elapsedMs = static_cast<long long>(std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count());
    double elapsedSec = elapsedMs / 1000.0;
    double speedMBps = (static_cast<double>(uploaded) / 1024.0 / 1024.0) / (elapsedSec > 0.0 ? elapsedSec : 1.0);

    {
        std::lock_guard<std::mutex> lg(m_csvMutex);
        std::ofstream ofs(m_reportFile, std::ios::app);
        ofs << filename << "," << localSize << "," << static_cast<uint64_t>(uploaded)
            << "," << elapsedMs << "," << speedMBps << "\n";
    }

    if (res != CURLE_OK || static_cast<uint64_t>(uploaded) != localSize) {
        std::cerr << "[upload] failed/incomplete: " << localPath
                  << " err=" << curl_easy_strerror(res)
                  << " uploaded=" << uploaded << " expected=" << localSize << "\n";
        return false;
    }

    // std::cout << "[upload] " << filename << " local=" << localSize
    //           << " uploaded=" << uploaded
    //           << " time_min=" << elapsedSec/60.0
    //           << " speed_MBps=" << speedMBps << "\n";
    return true;
}